Terminal Architecture
TheTerminal type in /internal/terminal/terminal.go wraps a PTY with terminal emulation:
Component Breakdown
- vt: Terminal emulator that parses ANSI/VT100 sequences
- ptmx: PTY master file handle (connected to shell process)
- cmd: Shell process (bash, zsh, etc.)
- width/height: Terminal dimensions in columns/rows
- workDir: Working directory for shell (room’s workspace)
- subscribers: Channels for broadcasting updates to clients
- lastRender: Cached string output (optimization)
- dirty: Flag indicating render cache needs refresh
Pseudo-Terminal (PTY) Basics
A PTY creates a master-slave pair:- Master: Application writes input, reads output
- Slave: Shell process thinks it’s a real terminal
Terminal Initialization
Starting the Terminal
- Create vt10x emulator with terminal size
- Determine shell executable (
$SHELLor/bin/sh) - Set working directory to room’s workspace
- Set
TERM=xterm-256colorenvironment variable - Start PTY with
creack/ptylibrary - Launch background goroutine to read shell output
Data Flow
Input Flow (Client → Shell)
- Bubble Tea converts keystroke to bytes (e.g.,
"a"→[]byte{0x61}, Enter →[]byte("\r")) terminal.Write(data)sends bytes to PTY master- PTY slave (shell) receives input as if from a real terminal
- Shell processes command and writes output
Output Flow (Shell → Clients)
- Read up to 4096 bytes from PTY master
- Feed bytes to vt10x emulator (
t.vt.Write(buf[:n])) - Mark render cache as dirty
- Broadcast update notification to all subscribers
vt10x Terminal Emulator
The vt10x emulator:- Parses ANSI/VT100 escape sequences (colors, cursor movement, etc.)
- Maintains a 2D grid of cells (each with a character and style)
- Tracks cursor position and visibility
- Handles terminal modes (insert, wrap, etc.)
Publisher-Subscriber Pattern
Subscription Management
Broadcasting Updates
select with default ensures slow clients don’t block the readLoop. If a client’s channel buffer is full, the update is skipped (client will get the next one).
Client Update Loop
In the Bubble Tea model:terminalUpdateMsg is received:
- Waits for terminal update notification
- Calls
terminal.Render()to get latest output - Updates UI model
- Starts waiting again
Rendering
Render Method
Rendering Optimizations
1. Caching:Color Conversion
- 0-7: Standard colors (black, red, green, yellow, blue, magenta, cyan, white)
- 8-15: Bright colors
- 16-255: Extended 256-color palette
Cursor Rendering
Window Resizing
- Update internal dimensions
- Invalidate render cache
- Resize vt10x emulator grid
- Send SIGWINCH to shell via
pty.Setsize()
Client Window Size Handling
When client terminal resizes:Terminal Cleanup
- Mark terminal as closed
- Close all subscriber channels (notifies clients)
- Close PTY master file descriptor
- Kill shell process
Shared Terminal State
All clients in a room share:- Same vt10x instance: Single source of truth for terminal state
- Same PTY: Input from any client goes to the same shell
- Same render output: All clients see identical terminal content
- Subscription channels: Each client has its own notification channel
- Render timing: Clients render independently based on their update loop
Performance Characteristics
Memory Usage
- vt10x grid:
width × height × sizeof(Cell)≈ 80 × 24 × 16 bytes = 30 KB - Render cache:
width × height × 4≈ 80 × 24 × 4 = 7.6 KB (ANSI sequences add overhead) - Read buffer: 4096 bytes per terminal
Latency
- Input latency: Direct write to PTY (< 1ms)
- Output latency:
- PTY read: kernel buffering (< 1ms)
- vt10x parse: O(n) in output bytes (< 1ms for typical output)
- Broadcast: O(clients), non-blocking
- Client render: Cached if no changes (< 1ms)
Scalability
Per room:- Terminal overhead: ~40 KB + shell process
- Per-client overhead: ~16 bytes (channel in subscriber map)
- Broadcast complexity: O(n) where n = number of clients
Keyboard Input Handling
Special key mappings in/internal/ui/model.go:
Error Handling
Shell Exit Detection
readLoop terminates gracefully.
Write Failures
Future Enhancements
Potential improvements:- Selective rendering: Only send diffs to clients instead of full frames
- Replay buffer: Store terminal history for late joiners
- Input queuing: Buffer input during network lag
- Compression: Compress render output for slow connections